package com.gitee.wsl.doc.prosemirror.model.node

import com.gitee.wsl.doc.prosemirror.exception.RangeError
import com.gitee.wsl.doc.prosemirror.model.attr.Attribute
import com.gitee.wsl.doc.prosemirror.model.attr.Attrs
import com.gitee.wsl.doc.prosemirror.model.content.ContentMatch
import com.gitee.wsl.doc.prosemirror.model.Schema
import com.gitee.wsl.doc.prosemirror.model.Whitespace
import com.gitee.wsl.doc.prosemirror.model.attr.defaultAttrs
import com.gitee.wsl.doc.prosemirror.model.fragment.Fragment
import com.gitee.wsl.doc.prosemirror.model.attr.initAttrs
import com.gitee.wsl.doc.prosemirror.model.mark.Mark
import com.gitee.wsl.doc.prosemirror.model.mark.MarkType
import com.gitee.wsl.doc.prosemirror.util.slice
import com.gitee.wsl.doc.prosemirror.util.verbose

// Node types are objects allocated once per `Schema` and used to [tag](#model.Node.type) `Node`
// instances. They contain information about the node type, such as its name and what kind of nod
// it represents.
class NodeType internal constructor(
    // The name the node type has in this schema.
    val name: String,
    // A link back to the `Schema` the node type belongs to.
    val schema: Schema,
    // The spec that this type is based on
    val spec: NodeSpec
) {
    internal val groups: List<String> = spec.group?.let { listOf(it) } ?: emptyList()
    internal val attrs: Map<String, Attribute> = initAttrs(name, spec.attrs)
    internal val defaultAttrs: Attrs = defaultAttrs(this.attrs)
    internal val defaultAttrsIncludingNullValues: Attrs = defaultAttrs(this.attrs, includeNullValues = true)

    // True if this node type has inline content.
    var inlineContent: Boolean = false

    // True if this is a block type
    val isBlock: Boolean = !(spec.inline || name == "text")

    // True if this is the text node type.
    val isText: Boolean = name == "text"

    // The starting match of the node type's content expression.
    lateinit var contentMatch: ContentMatch

    // The set of marks allowed in this node. `null` means all marks are allowed.
    var markSet: List<MarkType>? = null

    // True if this is an inline type.
    val isInline: Boolean
        get() = !this.isBlock

    // True if this is a textblock type, a block that contains inline content.
    val isTextblock: Boolean
        get() = this.isBlock && this.inlineContent

    val isLeaf: Boolean
        get() = this.contentMatch == ContentMatch.Companion.empty

    // True when this node is an atom, i.e. when it does not have directly editable content.
    val isAtom: Boolean
        get() = this.isLeaf || spec.atom

    // Return true when this node type is part of the given
    // [group](#model.NodeSpec.group).
    fun isInGroup(group: String) = this.groups.indexOf(group) > -1

    // If this is a node of the top level type (default: "doc")
    val isTopType: Boolean
        get() = this.name == (schema.spec.topNode ?: "doc")

    val whitespace: Whitespace
        get() = spec.whitespace ?: (if (this.spec.code) Whitespace.PRE else Whitespace.NORMAL)

    var creator: NodeCreator<out Node> = NodeCreator.DEFAULT

    // Tells you whether this node type has any required attributes.
    fun hasRequiredAttrs(): Boolean {
        return attrs.values.firstOrNull { it.isRequired } != null
    }

    // Indicates whether this node allows some of the same content as the given node type.
    fun compatibleContent(other: NodeType): Boolean {
        return this == other || this.contentMatch.compatible(other.contentMatch)
    }

    private fun computeAttrs(attrs: Attrs?): Attrs {
        return attrs ?: emptyAttrs
    }

    // Create a `Node` of this type. The given attributes are checked and defaulted (you can pass
    // `null` to use the type's defaults entirely, if no required attributes exist). `content` may
    // be a `Fragment`, a node, an array of nodes, or `null`. Similarly `marks` may be `null` to
    // default to the empty set of marks.
    fun create(attrs: Attrs? = null, content: Fragment? = null, marks: List<Mark>? = null): Node {
        if (this.isText) throw IllegalStateException("NodeType.create can't construct text nodes")
        return creator.create(this, this.computeAttrs(attrs), Fragment.Companion.from(content), Mark.Companion.setFrom(marks))
    }

    fun create(attrs: Attrs? = null, content: Node?, marks: List<Mark>? = null): Node {
        if (this.isText) throw IllegalStateException("NodeType.create can't construct text nodes")
        return creator.create(this, this.computeAttrs(attrs), Fragment.Companion.from(content), Mark.Companion.setFrom(marks))
    }

    fun create(attrs: Attrs? = null, content: List<Node>?, marks: List<Mark>? = null): Node {
        if (this.isText) throw IllegalStateException("NodeType.create can't construct text nodes")
        return creator.create(this, this.computeAttrs(attrs), Fragment.Companion.from(content), Mark.Companion.setFrom(marks))
    }

    // Like [`create`](#model.NodeType.create), but check the given content against the node type's
    // content restrictions, and throw an error if it doesn't match.
    fun createChecked(attrs: Attrs? = null, content: Fragment? = null, marks: List<Mark>? = null): Node {
        val thisContent = Fragment.Companion.from(content)
        this.checkContent(thisContent)
        return creator.create(this, this.computeAttrs(attrs), thisContent, Mark.Companion.setFrom(marks))
    }

    fun createChecked(attrs: Attrs? = null, content: Node?, marks: List<Mark>? = null): Node {
        val thisContent = Fragment.Companion.from(content)
        if (!this.validContent(thisContent)) {
            throw RangeError(
                if (verbose) {
                    "Invalid content for node type $name: $thisContent"
                } else {
                    "Invalid content for node type $name"
                }
            )
        }
        return creator.create(this, computeAttrs(attrs), thisContent, Mark.Companion.setFrom(marks))
    }

    fun createChecked(attrs: Attrs? = null, content: List<Node>?, marks: List<Mark>? = null): Node {
        val thisContent = Fragment.from(content)
        if (!this.validContent(thisContent)) {
            throw RangeError(
                if (verbose) {
                    "Invalid content for node type $name: $thisContent"
                } else {
                    "Invalid content for node type $name"
                }
            )
        }
        return creator.create(this, this.computeAttrs(attrs), thisContent, Mark.setFrom(marks))
    }

    // Like [`create`](#model.NodeType.create), but see if it is necessary to add nodes to the start
    // or end of the given fragment to make it fit the node. If no fitting wrapping can be found,
    // return null. Note that, due to the fact that required nodes can always be created, this will
    // always succeed if you pass null or `Fragment.empty` as content.
    fun createAndFill(attrs: Attrs? = null, content: Fragment? = null, marks: List<Mark>? = null): Node? {
        val attrs = this.computeAttrs(attrs)
        var content = Fragment.from(content)
        if (content.size != 0) {
            val before = this.contentMatch.fillBefore(content) ?: return null
            content = before.append(content)
        }
        val matched = this.contentMatch.matchFragment(content)
        val after = matched?.fillBefore(Fragment.empty, true) ?: return null
        return creator.create(this, attrs, content.append(after), Mark.setFrom(marks))
    }

    fun createAndFill(attrs: Attrs? = null, content: Node?, marks: List<Mark>?): Node? {
        val attrs = this.computeAttrs(attrs)
        var content = Fragment.from(content)
        if (content.size != 0) {
            val before = this.contentMatch.fillBefore(content) ?: return null
            content = before.append(content)
        }
        val matched = this.contentMatch.matchFragment(content)
        val after = matched?.fillBefore(Fragment.empty, true) ?: return null
        return creator.create(this, attrs, content.append(after), Mark.setFrom(marks))
    }

    fun createAndFill(attrs: Attrs? = null, content: List<Node>?, marks: List<Mark>?): Node? {
        val attrs = this.computeAttrs(attrs)
        var content = Fragment.from(content)
        if (content.size != 0) {
            val before = this.contentMatch.fillBefore(content) ?: return null
            content = before.append(content)
        }
        val matched = this.contentMatch.matchFragment(content)
        val after = matched?.fillBefore(Fragment.empty, true) ?: return null
        return creator.create(this, attrs, content.append(after), Mark.setFrom(marks))
    }

    // Returns true if the given fragment is valid content for this node
    // type.
    fun validContent(content: Fragment): Boolean {
        val result = this.contentMatch.matchFragment(content)
        if (result == null || !result.validEnd) {
            return false
        }
        for (i in 0 until content.childCount) {
            if (!this.allowsMarks(content.child(i).marks)) {
                return false
            }
        }
        return true
    }

    // Throws a RangeError if the given fragment is not valid content for this
    // node type.
    internal fun checkContent(content: Fragment) {
        if (!this.validContent(content)) {
            throw RangeError(
                if (verbose) {
                    "Invalid content for node $name: ${content.toString().slice(0, 50)}"
                } else {
                    "Invalid content for node $name"
                }
            )
        }
    }

    internal fun checkAttrs(attrs: Attrs) {
        com.gitee.wsl.doc.prosemirror.model.attr.checkAttrs(this.attrs, attrs, "node", this.name)
    }

    // Check whether the given mark type is allowed in this node.
    fun allowsMarkType(markType: MarkType): Boolean {
        val markSet = this.markSet
        return markSet == null || markSet.indexOf(markType) > -1
    }

    // Test whether the given set of marks are allowed in this node.
    fun allowsMarks(marks: List<Mark>): Boolean {
        if (this.markSet == null) return true
        marks.forEach {
            if (!this.allowsMarkType(it.type)) return false
        }
        return true
    }

    // Removes the marks that are not allowed in this node from the given set.
    fun allowedMarks(marks: List<Mark>): List<Mark> {
        if (this.markSet == null) return marks
        var copy: MutableList<Mark>? = null
        marks.forEachIndexed { i, mark ->
            if (!this.allowsMarkType(mark.type)) {
                if (copy == null) copy = marks.slice(0..<i).toMutableList()
            } else {
                copy?.add(mark)
            }
        }
        val res = copy
        return when {
            res == null -> marks
            res.isEmpty() -> Mark.none
            else -> res
        }
    }

    override fun toString(): String {
        return "NodeType {name: '$name', schema: $schema, spec: $spec, markSet: $markSet, groups: $groups }"
    }

    companion object {
        internal fun compile(nodes: Map<String, NodeSpec>, schema: Schema): Map<String, NodeType> {
            val result = mutableMapOf<String, NodeType>()
            nodes.forEach { (name, spec) ->
                result[name] = NodeType(name, schema, spec)
            }

            val topType = schema.spec.topNode ?: "doc"
            if (!result.containsKey(topType)) throw RangeError("Schema is missing its top node type ('$topType')")
            if (!result.containsKey("text")) throw RangeError("Every schema needs a 'text' type")
            val textSpec = result["text"]!!
            if (textSpec.attrs.isNotEmpty()) throw RangeError("The text node type should not have attributes")

            return result
        }
    }
}

fun NodeType.isDefinesContent() = spec.defining == true || spec.definingForContent == true
